﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Web;
using TripLOA.WebService.Serializable;

namespace TripLOA.WebService.Rest
{
    public abstract class EndPoint : IHttpHandler, System.Web.SessionState.IRequiresSessionState
    {
        public Type classType { get; set; }

        #region Managed methods 

        public void ProcessRequest(HttpContext context)
        {
            string response = ""; //output response
            classType = this.GetType();

            NameValueCollection _REQUEST = context.Request.QueryString; //_GET Parameters

            string format = _REQUEST["format"]; //Response format
            switch (format)
            {
                case "json":
                    context.Response.ContentType = "text/plain";
                    break;

                default:
                    format = "rest";
                    context.Response.ContentType = "text/xml";
                    break;
            }

            string method = _REQUEST["method"]; //Method to invoke
            if (method == null)
            {
                //method missing, return WSError
                response = Serialization.serialize(new WSError() { ErrorType = ErrorType.RequiredParameter, Message = "method parameter is required" },
                                                    typeof(WSError),
                                                    format);
                context.Response.Write(response);
                return;
            }
            else
            {
                //Check if method really exists
                MethodInfo[] mInfo = classType.GetMethods();
                bool valid = false;
                foreach (MethodInfo mi in mInfo)
                {
                    if (mi.Name == method)
                    {
                        valid = true; //Method found!
                        break;
                    }
                }

                if (!valid)
                {
                    //If requested method is not a valid method return an error
                    response = Serialization.serialize(new WSError() { ErrorType = ErrorType.InvalidParameter, Message = "method parameter is invalid" },
                                                    typeof(WSError),
                                                    format);
                    context.Response.Write(response);
                    return;
                }
            }

            try
            {
                response = (string)classType.InvokeMember(method,
                                                        BindingFlags.Default | BindingFlags.InvokeMethod,
                                                        null,
                                                        this,
                                                        new Object[] { _REQUEST, format }
                                                      );
            }
            catch (Exception ex)
            {
                //Exception during method invocation, return WSError
                response = Serialization.serialize(new WSError() { ErrorType = ErrorType.MethodException, Message = "Exception during method invocation: " + ex.Message },
                                                    typeof(WSError),
                                                    format);
            }


            context.Response.Write(response);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

        #endregion


        #region Parsing

        /// <summary>
        /// Parse an object field given the input key as String
        /// </summary>
        /// <param name="obj">The Dictionary that represents the object</param>
        /// <param name="key">Key as String</param>
        /// <param name="value">Value</param>
        /// <example>ParseObjectField(dict, "obj[0][field1][1]", "valueForThatField")</example>
        protected static void ParseObjectField(Dictionary<string, object> obj, string key, string value)
        {
            //string [] names = key.Split('[', ']');
            string[] names = key.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);

            Dictionary<string, object> iter = obj;
            string nameIter;

            string lastName = names.Last();

            foreach (string name in names)
            {
                if (name == lastName) continue; //Ignore empty string

                nameIter = name;
                if (!iter.ContainsKey(name))
                {
                    iter.Add(name, new Dictionary<string, object>());
                }

                iter = (Dictionary<string, object>)iter[name];
            }

            iter.Add(lastName, value);
        }

        /// <summary>
        /// Create a RestObject. It is represented as a Dictionary
        /// </summary>
        /// <param name="_REQUEST">The GET Request with serialized Object</param>
        /// <param name="restObjectName">The object name in serialized string</param>
        /// <returns>The Dictionary representing the Object</returns>
        protected static Dictionary<string, object> CreateRestObject(NameValueCollection _REQUEST, string restObjectName)
        {
            Dictionary<string, object> restObj = new Dictionary<string, object>();
            
            foreach (string key in _REQUEST.Keys)
            {
                if (key.StartsWith(restObjectName+"["))
                {
                    EndPoint.ParseObjectField(restObj, key, _REQUEST[key]);
                }
            }

            return (Dictionary<string, object>)restObj[restObjectName];
        }

        /// <summary>
        /// Create an object from a RestObject <see cref="CreateRestObject"/>
        /// </summary>
        /// <param name="restObj">The Rest Object</param>
        /// <param name="objType">The object type to create</param>
        /// <returns>The real object</returns>
        protected static object FillObject(Dictionary<string, object> restObj, Type objType)
        {
            //Instantiate a new object
            var obj = objType.GetConstructor(Type.EmptyTypes).Invoke(null);

            //If object type is a List scan restObj differently
            if (objType.Name.StartsWith("List"))
            {
                //Get the generic list type
                var args = objType.GetGenericArguments();
                var listType = args.First();

                //Scan the Rest Object Array
                foreach (var rKey in restObj.Keys)
                {
                    //Get the value for list item
                    object value = EndPoint.FillObject((Dictionary<string, object>)restObj[rKey], listType);
                    //Add list item
                    objType.GetMethod("Add", new Type[] { listType }).Invoke(obj, new object[] { value });
                }
                //Return list
                return obj;
            }

            //Get the object property list
            PropertyInfo[] properties = objType.GetProperties();

            //Scan property list
            foreach (var p in properties)
            {
                //Check if field is filled in Rest Object
                if(!restObj.ContainsKey(p.Name)) continue;
                
                //If property is a Nullable get the value property
                PropertyInfo pi;
                if (p.PropertyType.Name.StartsWith("Nullable"))
                {
                    pi = p.PropertyType.GetProperty("Value");
                }
                else
                {
                    pi = p;
                }

                //Check if property type has method Parse
                MethodInfo parse = pi.PropertyType.GetMethod("Parse", new Type[] { typeof(String) });

                if (parse != null)
                {
                    //If property is a primitive type performe a direct assignment
                    string value = (string)restObj[p.Name];
                    var result = parse.Invoke(null, new object[] { value });
                    p.SetValue(obj, result, null);
                }
                else if (pi.PropertyType.Equals(typeof(String)))
                {
                    //If property is a string performe a direct assigment
                    p.SetValue(obj, restObj[p.Name], null);
                }
                else if (pi.PropertyType.IsEnum)
                {
                    //If property is an enum try to assign an enum value
                    var value = pi.PropertyType.GetField((string)restObj[p.Name]);
                    p.SetValue(obj, value.GetValue(pi.PropertyType), null);
                }
                else
                {
                    //Else fill object
                    p.SetValue(obj, EndPoint.FillObject((Dictionary<string, object>)restObj[p.Name], pi.PropertyType), null);
                }
            }

            return obj;
        }

        #endregion
    }
}
